Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Playwright: use an interface for passing TestFile paths. #55693

Merged
merged 3 commits into from
Sep 9, 2021

Conversation

worldomonation
Copy link
Contributor

Changes proposed in this Pull Request

This PR defines and implements a TypeScript interface to pass test file paths around.

Key changes:

  • interface defined to hold multiple parts of the filepath of a test file.
  • update test specs.

Testing instructions

Fixes #55641

@matticbot
Copy link
Contributor

This PR does not affect the size of JS and CSS bundles shipped to the user's browser.

Generated by performance advisor bot at iscalypsofastyet.com.

@worldomonation worldomonation force-pushed the pw/test-file-interface branch from 9879e6c to 72b7813 Compare August 24, 2021 21:12
@matticbot
Copy link
Contributor

matticbot commented Aug 24, 2021

Comment on lines 87 to 85
const extension = sourceFileName.split( '.' ).pop();
if ( ! extension ) {
throw new Error( `Extension not found on source file ${ sourceFileName }` );
}
const basename = `${ filename }.${ sourceFileName.split( '.' ).pop() }`;
// Create test files in the same directory as the source file.
const dirname = path.join( __dirname, '../../../../../test/e2e/image-uploads/' );
// Full path on disk of the source file, to be copied and renamed.
const sourceFilePath = path.join( dirname, sourceFileName );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous code did not survive the 3-month later check; I was confused by the reassignment of variables stemming from my attempts to limit the number of constants.

I've gone the other way now with this PR and creating single-use constants that better reflect the individual parts of the path, which can be assembled into an object implementing the interface at end.

@worldomonation worldomonation force-pushed the pw/test-file-interface branch from 5b6a182 to c348d1d Compare August 25, 2021 20:30
'..',
'image-uploads',
'unsupported_extension.mkv'
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file defines the paths where the test files are located.

Test writers should import the appropriate const and pass it into the createTestFile method.

page = args.page;
} );

beforeAll( async () => {
logoImage = await MediaHelper.createTestImage();
logoImage = await MediaHelper.createTestFile( TEST_IMAGE_PATH );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All other methods for generating test files have been removed. Test writers are expected to pass the path of the source image file to the createTestFile method.

} );

describe.each`
siteType | user
${ 'Simple' } | ${ 'defaultUser' }
${ 'Atomic' } | ${ 'wooCommerceUser' }
`( 'Edit Image ($siteType)', function ( { user } ) {
let mediaPage;
let mediaPage: MediaPage;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file has been changed to TypeScript.

"include": [ "specs/specs-playwright" ] // TypeScript is scoped only for the new Playwright scripts
"types": [ "jest" ] // no mocha - we are only using TypeScript for the new Playwright scripts
},
"include": [ "specs/specs-playwright", "specs/constants.js" ] // TypeScript is scoped only for the new Playwright scripts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the constants.js file is located within specs/specs-playwright, it causes Jest to attempt to execute the file as a test. This then fails due to the constants.js file not being a test.

@worldomonation worldomonation linked an issue Aug 25, 2021 that may be closed by this pull request
@worldomonation worldomonation marked this pull request as ready for review August 25, 2021 23:39
@worldomonation worldomonation requested review from scinos, WunderBart and a team as code owners August 25, 2021 23:39
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Aug 25, 2021
} = {}
): Promise< TestFile > {
try {
await fs.open( sourcePath, 'r' );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain what are we trying to do here? Seems weird to open a file descriptor and not use it (plus we should close the fd later).

Is the goal to check that the file exists and is readable? In that case maybe we can use fs.access()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent is to check whether the file exists first.
With that said, it seems something else is swallowing the error - an invalid path given to this method didn't output stack trace as I would have expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like throw Error is being swallowed since the code calling createTestFile is within a hook.

I've changed the throw statements to instead console.error and have verified this isn't swallowed up:

  console.error
    Source file /Users/edwintakahashi/workspace/wp-calypso-third/test/e2e/specs/image-uploads/image0.jpg not found on disk.

      80 | 		await fs.access( sourcePath );
      81 | 	} catch {
    > 82 | 		console.error( `Source file ${sourcePath} not found on disk.` );
         | 		        ^
      83 | 		// throw new Error( `Source file ${ sourcePath } not found on disk.` );
      84 | 	}
      85 |

      at Object.createTestFile (../../packages/calypso-e2e/src/media-helper.ts:82:11)
      at specs/specs-playwright/wp-blocks__media-spec.ts:27:11

This is a common topic of discussion as shown here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5d4b3c3.

const tempDir = await fs.mkdtemp( path.join( os.tmpdir(), 'e2e-' ) );
const testFilePath = path.join( tempDir, fileName );
// Generate a filename using current timestamp and a pseudo-randomly generated integer.
let filename = `${ getTimestamp() }-${ getRandomInteger( 100, 999 ) }`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fs.mkdtemp() "Creates a unique temporary directory. A unique directory name is generated by appending six random characters to the end of the provided prefix" (from the docs).

Do we still need to append a random integer to the filename?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The uniqueness here isn't about the directory/path per se, but rather has to do with the filename portion.

If two tests both end up creating a test file named identical_filename.jpg, uploading this to WPCOM would result in one instance of the file being renamed with a -1 postfix.
Later down the test, there are checks for whether the expected file name is found on the page. The -1 postfix interferes with this.

Another solution is to switch the validation to use a regex to match the important portion of the file name eg. 167888888.*.jpg.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the postfix in cbd61f3.

export async function createTestAudio(): Promise< string > {
return await createTestFile( { sourceFileName: 'bees.mp3' } );
}
await fs.copyFile( sourcePath, targetPath );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use mode COPYFILE_EXCL to fail if the target already exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 5d4b3c3.

@@ -0,0 +1,10 @@
import path from 'path';

export const TEST_IMAGE_PATH = path.resolve( __dirname, '..', 'image-uploads', 'image0.jpg' );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of path.resolve() is to find an absolute path appending from right to left, I think is misused here.

Instead we can use any of these:

path.join(__dirname, '../image-uploads/image0.jpg')
path.normalize(`${__dirname}/../image-uploads/image0.jpg`)

Copy link
Contributor Author

@worldomonation worldomonation Aug 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I had assumed the resolve would mean it joins all of them then resolves the resulting string to a path on the disk. Not the best name in my opinion.

I will choose path.normalize(`${__dirname}/../image-uploads/image0.jpg`).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to a combination of path.normalize(path.join()) in 5d4b3c3.

@worldomonation worldomonation force-pushed the pw/test-file-interface branch from 3694103 to 5d4b3c3 Compare August 26, 2021 20:51
@worldomonation worldomonation self-assigned this Aug 30, 2021
@worldomonation worldomonation requested a review from scinos August 30, 2021 19:58
@worldomonation
Copy link
Contributor Author

That issue is 2 years old, and a few comments suggests errors work as expected with jest-circus (which we are already using).

What behaviour do you see with the throw?

This is what happens if throw is used:

  pw:api => browserType.launch started +0ms
  pw:api <= browserType.launch succeeded +1s
  pw:api => browser.newContext started +1ms
  pw:api <= browser.newContext succeeded +10ms
  pw:api => browserContext.newPage started +1ms
  pw:api <= browserContext.newPage succeeded +467ms
  pw:api => page.screenshot started +39ms
  pw:api <= page.screenshot succeeded +112ms
  pw:api => page.close started +1ms
  pw:api <= page.close succeeded +10ms
  pw:api => browser.close started +1ms
  pw:api <= browser.close succeeded +253ms
Test Suites: 1 skipped, 0 of 1 total
Tests:       12 skipped, 12 total
Snapshots:   0 total
Time:        2.833 s, estimated 28 s

Notice that Jest appears to be swallowing up the exception.

I can replicate this with this:

beforeAll( async () => {
		throw new Error( 'hook error' );
	} );

With console.error:

  console.error
    Source file /Users/edwintakahashi/workspace/wp-calypso/test/e2e/image-uploads/image0.jpg not found on disk.

      78 | 		await fs.access( '' );
      79 | 	} catch {
    > 80 | 		console.error( `Source file ${ sourcePath } not found on disk.` );
         | 		        ^
      81 | 	}
      82 |
      83 | 	// Obtain the file extension.

      at Object.createTestFile (../../packages/calypso-e2e/src/media-helper.ts:80:11)
      at specs/specs-playwright/wp-media__upload-spec.ts:24

@worldomonation
Copy link
Contributor Author

worldomonation commented Aug 30, 2021

Now, the way I had it wasn't correct either since console.error does not actually halt execution. I've fixed that with:

console.error('message')
throw new Error()

This way, the error message is not suppressed and the execution is halted as expected.

@scinos
Copy link
Contributor

scinos commented Aug 31, 2021

Looks like the problem is our custom environment.

As you found out, when a beforeHook throws an error it halts the execution but the actual message is never displayed. However, commenting out testEnvironment in test/e2e/jest.config.js does solve the problem (the error is shown):

image

Maybe we should have a console.error() in test/e2e/lib/jest/environment.js, in the failure cases

@worldomonation
Copy link
Contributor Author

Looks like the problem is our custom environment.

As you found out, when a beforeHook throws an error it halts the execution but the actual message is never displayed. However, commenting out testEnvironment in test/e2e/jest.config.js does solve the problem (the error is shown):

image

Maybe we should have a console.error() in test/e2e/lib/jest/environment.js, in the failure cases

Thanks for the pointer. I only had a brief look at environment.js in the past and this was a good chance to look a bit deeper. This has the potential to do a lot of interesting things.

With my changes in a529ebc I see the following when a hook fails:

  pw:api => browserType.launch started +0ms
  pw:api <= browserType.launch succeeded +2s
  pw:api => browser.newContext started +1ms
  pw:api <= browser.newContext succeeded +7ms
  pw:api => browserContext.newPage started +2ms
  pw:api <= browserContext.newPage succeeded +488ms
Error: Source file /Users/edwintakahashi/workspace/wp-calypso/test/e2e/image-uploads/image0.jpg not found on disk.
    at Object.createTestFile (/Users/edwintakahashi/workspace/wp-calypso/packages/calypso-e2e/src/media-helper.ts:80:9)
    at /Users/edwintakahashi/workspace/wp-calypso/test/e2e/specs/specs-playwright/wp-media__upload-spec.ts:24:11
  pw:api => page.close started +31ms
  pw:api <= page.close succeeded +8ms
  pw:api => video.delete started +1ms
  pw:api <= video.delete succeeded +63ms
  pw:api => browser.close started +0ms
  pw:api <= browser.close succeeded +214ms
Test Suites: 1 skipped, 0 of 1 total
Tests:       12 skipped, 12 total
Snapshots:   0 total
Time:        3.227 s, estimated 4 s

What do you think?

@scinos
Copy link
Contributor

scinos commented Sep 2, 2021

I swear I added a comment on the changes in environment.js but I can't find it now :(
Sorry for the delay.

Once the comment about this.global.__CURRENT_TEST_FAILED__ has been addressed, this is ready to go.

@@ -19,6 +19,9 @@ class JestEnvironmentE2E extends JestEnvironmentNode {
break;

case 'hook_failure':
console.error( event.error );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to log the error in case of test_fn_failure? Does jest does it automatically?

At the very least I think we should set this.global.__CURRENT_TEST_FAILED__ = true; here. That global property is checked by the screenshot/video recorder hooks.

Most of the time a failed hook mean there is nothing to take a screenshot from, but we can't guarantee that will be the case forever (for example, we may have a hook that opens the browser and does some navigation, and we do want to screenshot that if it fails).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my comment also went missing too 👀

We don't want to log the error in case of test_fn_failure? Does jest does it automatically?

In our current configuration, Jest automatically throws and outputs the relevant logs if the error is thrown outside of a hook.

At the very least I think we should set this.global.CURRENT_TEST_FAILED = true; here. That global property is checked by the screenshot/video recorder hooks.

Avoiding the screenshot/video recording codepath was why initially the value was not set. But as you mention:

we can't guarantee that will be the case forever (for example, we may have a hook that opens the browser and does some navigation, and we do want to screenshot that if it fails)

Yes, that is true. I will treat hook failure like a normal test failure in next commit.

Copy link
Contributor Author

@worldomonation worldomonation Sep 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0581a7d.

Revised behavior now outputs the following if anything in the hook fails:

 FAIL  specs/specs-playwright/wp-media__edit-spec.ts
  ● Test suite failed to run

    Source file /Users/edwintakahashi/workspace/wp-calypso-second/test/e2e/image-uploads/image0.jpg not found on disk.

      78 | 		await fs.access( 'sourcePath' );
      79 | 	} catch {
    > 80 | 		throw new Error( `Source file ${ sourcePath } not found on disk.` );
         | 		      ^
      81 | 	}
      82 |
      83 | 	// Obtain the file extension.

      at Object.createTestFile (../../packages/calypso-e2e/src/media-helper.ts:80:9)
      at specs/specs-playwright/wp-media__edit-spec.ts:22:15

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        2.705 s

But I suspect throw event.error will prevent rest of the beforeAll or afterAll hook from executing, so this will likely not capture screenshots.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've revised the behavior again in 87006be since I felt the previous behavior was a bit hacky.

Recap: throwing an error in environment.js causes rest of execution to halt. Practically, this means the afterAll hooks of screenshot/video capture are not run.

I've changed the behavior to instead do the following:

  1. if a hook failure is detected, set a new attribute hookFailed.
  2. at test_start event, check if hook has failed.
  3. if hook has failed, mark all test cases in the describe block as failed.

I arrived at this solution because these lines were causing hook failures to be treated like a normal skip and ended up suppressing the errors thrown. To verify this, add an exception in a hook, modify the skip to a fail. Errors thrown in the hook should now be visible.

The separate treatment of hook failure is required, otherwise if an exception is thrown in the test step it would not be skipped. This can be verified by performing the same steps as above, but place the exception in the test step instead. Jest will not skip rest of the test steps.

Try pulling my changes in this revision, place exceptions in beforeAll hook, test step and afterAll hook. It should behave following this rule:

  1. if exception is thrown in a hook, suite is marked as failed and test steps yet to run are also marked as failed.
  2. if exception is thrown in a step, only the failing step is marked failed and rest are skipped.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scenario 1:

  ● [WPCOM] Media: Edit Media: (desktop) @parallel › Edit Image (Atomic) › Cancel image edit

    hello!

      20 |
      21 | 	beforeAll( async () => {
    > 22 | 		throw new Error( 'hello!' );
         | 		      ^
      23 | 		testImage = await MediaHelper.createTestFile( TEST_IMAGE_PATH );
      24 | 	} );
      25 |

      at specs/specs-playwright/wp-media__edit-spec.ts:22:9

Test Suites: 1 failed, 1 total
Tests:       16 failed, 16 total
Snapshots:   0 total
Time:        3.14 s, estimated 12 s

Scenario 2:

  ● [WPCOM] Media: Edit Media: (desktop) @parallel › Edit Image (Simple) › Log In

    hello!

      31 |
      32 | 		it( 'Log In', async function () {
    > 33 | 			throw new Error( 'hello!' );
         | 			      ^
      34 | 			const loginFlow = new LoginFlow( page, user );
      35 | 			await loginFlow.logIn();
      36 | 		} );

      at Object.<anonymous> (specs/specs-playwright/wp-media__edit-spec.ts:33:10)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 15 skipped, 16 total
Snapshots:   0 total
Time:        3.078 s, estimated 4 s

Scenario 3:

  ● Test suite failed to run

    hello!

      60 |
      61 | 		afterAll( async function () {
    > 62 | 			throw new Error( 'hello!' );
         | 			      ^
      63 | 		} );
      64 | 	} );
      65 | } );

      at specs/specs-playwright/wp-media__edit-spec.ts:62:10
          at runMicrotasks (<anonymous>)

Test Suites: 1 failed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        11.912 s, estimated 43 s

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scinos Could I please get a review on this before it gets too stale? Thanks!

Copy link
Contributor

@scinos scinos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢

- update Media (Upload) spec.
- update CoBlocks spec.
- update Blocks Media (Upload) spec.
Move Media Upload spec to TypeScript.

Update Media (Edit) spec
- new file `constants.js` to hold the source paths for the test files.
- call `createTestFile` by default and provide path.
- refactor the createTestFile method to use better variable names.

Revise behavior when hook fails.

Revise environment behavior again.
- set the `hookFailed` attribute to true if hook failure is experienced.
- add clause at `test_start` event to check for hook failure, and if detected, mark rest of the test cases as failed.

Rebase.
@worldomonation worldomonation merged commit 091f787 into trunk Sep 9, 2021
@worldomonation worldomonation deleted the pw/test-file-interface branch September 9, 2021 16:58
@github-actions github-actions bot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Sep 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants